package com.jse.web;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.BiConsumer;

public class MultipartWriter {

    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    private static final byte[] FIELD_SEP = ": ".getBytes(StandardCharsets.ISO_8859_1);
    private static final byte[] CR_LF = "\r\n".getBytes(StandardCharsets.ISO_8859_1);
    private static final String TWO_HYPHENS_TEXT = "--";
    private static final byte[] TWO_HYPHENS = TWO_HYPHENS_TEXT.getBytes(StandardCharsets.ISO_8859_1);
    private static final String CONTENT_DISPOSITION_KEY = "Content-Disposition";
    private static final String CONTENT_TYPE_KEY = "Content-Type";
    private static final String DEFAULT_CONTENT_TYPE = "multipart/form-data; boundary=";
    private static final String DEFAULT_BINARY_CONTENT_TYPE = "application/octet-stream";
    private static final String DEFAULT_TEXT_CONTENT_TYPE = "text/plain;charset=UTF-8";
    private static final String DEFAULT_CONTENT_DISPOSITION_VALUE = "form-data; name=\"%s\"";
    private static final String FILE_CONTENT_DISPOSITION_VALUE = "form-data; name=\"%s\"; filename=\"%s\"";

    private final Map<String, String> headers = new HashMap<>(8);
    private final List<AbstractMultipartPart> parts = new ArrayList<>();
    private final String boundary;

    private MultipartWriter(String boundary) {
        this.boundary = Objects.isNull(boundary) ? TWO_HYPHENS_TEXT +
                UUID.randomUUID().toString().replace("-", "") : boundary;
        this.headers.put(CONTENT_TYPE_KEY, DEFAULT_CONTENT_TYPE + this.boundary);
    }

    public static MultipartWriter newMultipartWriter(String boundary) {
        return new MultipartWriter(boundary);
    }

    public static MultipartWriter newMultipartWriter() {
        return new MultipartWriter(null);
    }

    public MultipartWriter addHeader(String key, String value) {
        if (!CONTENT_TYPE_KEY.equalsIgnoreCase(key)) {
            headers.put(key, value);
        }
        return this;
    }

    public MultipartWriter addTextPart(String name, String text) {
        parts.add(new TextPart(String.format(DEFAULT_CONTENT_DISPOSITION_VALUE, name), DEFAULT_TEXT_CONTENT_TYPE, this.boundary, text));
        return this;
    }

    public MultipartWriter addBinaryPart(String name, byte[] bytes) {
        parts.add(new BinaryPart(String.format(DEFAULT_CONTENT_DISPOSITION_VALUE, name), DEFAULT_BINARY_CONTENT_TYPE, this.boundary, bytes));
        return this;
    }

    public MultipartWriter addFilePart(String name, File file) {
        parts.add(new FilePart(String.format(FILE_CONTENT_DISPOSITION_VALUE, name, file.getName()), DEFAULT_BINARY_CONTENT_TYPE, this.boundary, file));
        return this;
    }

    private static void writeHeader(String key, String value, OutputStream out) throws IOException {
        writeBytes(key, out);
        writeBytes(FIELD_SEP, out);
        writeBytes(value, out);
        writeBytes(CR_LF, out);
    }

    private static void writeBytes(String text, OutputStream out) throws IOException {
        out.write(text.getBytes(DEFAULT_CHARSET));
    }

    private static void writeBytes(byte[] bytes, OutputStream out) throws IOException {
        out.write(bytes);
    }

    interface MultipartPart {

        void writeBody(OutputStream os) throws IOException;
    }

    public static abstract class AbstractMultipartPart implements MultipartPart {

        protected final String contentDispositionValue;
        protected final String contentTypeValue;
        protected final String boundary;
        

        public AbstractMultipartPart(String contentDispositionValue, String contentTypeValue, String boundary) {
			this.contentDispositionValue = contentDispositionValue;
			this.contentTypeValue = contentTypeValue;
			this.boundary = boundary;
		}

		protected String getContentDispositionValue() {
            return contentDispositionValue;
        }

        protected String getContentTypeValue() {
            return contentTypeValue;
        }

        protected String getBoundary() {
            return boundary;
        }

        public final void write(OutputStream out) throws IOException {
            writeBytes(TWO_HYPHENS, out);
            writeBytes(getBoundary(), out);
            writeBytes(CR_LF, out);
            writeHeader(CONTENT_DISPOSITION_KEY, getContentDispositionValue(), out);
            writeHeader(CONTENT_TYPE_KEY, getContentTypeValue(), out);
            writeBytes(CR_LF, out);
            writeBody(out);
            writeBytes(CR_LF, out);
        }
    }

    public static class TextPart extends AbstractMultipartPart {

        private final String text;

        public TextPart(String contentDispositionValue,
                        String contentTypeValue,
                        String boundary,
                        String text) {
            super(contentDispositionValue, contentTypeValue, boundary);
            this.text = text;
        }

        @Override
        public void writeBody(OutputStream os) throws IOException {
            os.write(text.getBytes(DEFAULT_CHARSET));
        }

        @Override
        protected String getContentDispositionValue() {
            return contentDispositionValue;
        }

        @Override
        protected String getContentTypeValue() {
            return contentTypeValue;
        }
    }

    public static class BinaryPart extends AbstractMultipartPart {

        private final byte[] content;

        public BinaryPart(String contentDispositionValue,
                          String contentTypeValue,
                          String boundary,
                          byte[] content) {
            super(contentDispositionValue, contentTypeValue, boundary);
            this.content = content;
        }

        @Override
        public void writeBody(OutputStream out) throws IOException {
            out.write(content);
        }
    }

    public static class FilePart extends AbstractMultipartPart {

        private final File file;

        public FilePart(String contentDispositionValue,
                        String contentTypeValue,
                        String boundary,
                        File file) {
            super(contentDispositionValue, contentTypeValue, boundary);
            this.file = file;
        }

        @Override
        public void writeBody(OutputStream out) throws IOException {
            try (InputStream in = new FileInputStream(file)) {
                final byte[] buffer = new byte[4096];
                int l;
                while ((l = in.read(buffer)) != -1) {
                    out.write(buffer, 0, l);
                }
                out.flush();
            }
        }
    }

    public void forEachHeader(BiConsumer<String, String> consumer) {
        headers.forEach(consumer);
    }

    public void write(OutputStream out) throws IOException {
        if (!parts.isEmpty()) {
            for (AbstractMultipartPart part : parts) {
                part.write(out);
            }
        }
        writeBytes(TWO_HYPHENS, out);
        writeBytes(this.boundary, out);
        writeBytes(TWO_HYPHENS, out);
        writeBytes(CR_LF, out);
    }
}